diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0340b92 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.o +*.a +*.gch diff --git a/src/optparse.c b/src/optparse.c new file mode 100644 index 0000000..867b635 --- /dev/null +++ b/src/optparse.c @@ -0,0 +1,391 @@ +/** @file + Option parsing functions to complement getopt. + + Copyright (C) 2017 Christian Zuckschwerdt + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. +*/ + +#include "optparse.h" +#include +#include +#include +#include + +int atobv(char *arg, int def) +{ + if (!arg) + return def; + if (!strcasecmp(arg, "true") || !strcasecmp(arg, "yes") || !strcasecmp(arg, "on") || !strcasecmp(arg, "enable")) + return 1; + return atoi(arg); +} + +int atoiv(char *arg, int def) +{ + if (!arg) + return def; + char *endptr; + int val = strtol(arg, &endptr, 10); + if (arg == endptr) + return def; + return val; +} + +char *arg_param(char *arg) +{ + if (!arg) + return NULL; + char *p = strchr(arg, ':'); + char *c = strchr(arg, ','); + if (p && (!c || p < c)) + return ++p; + else if (c) + return c; + else + return p; +} + +double arg_float(const char *str, const char *error_hint) +{ + if (!str) { + fprintf(stderr, "%smissing number argument\n", error_hint); + exit(1); + } + + if (!*str) { + fprintf(stderr, "%sempty number argument\n", error_hint); + exit(1); + } + + // allow whitespace and equals char + while (*str == ' ' || *str == '=') + ++str; + + char *endptr; + double val = strtod(str, &endptr); + + if (str == endptr) { + fprintf(stderr, "%sinvalid number argument (%s)\n", error_hint, str); + exit(1); + } + + return val; +} + +char *hostport_param(char *param, char **host, char **port) +{ + if (param && *param) { + if (param[0] == '/' && param[1] == '/') { + param += 2; + } + if (*param != ':' && *param != ',') { + *host = param; + if (*param == '[') { + (*host)++; + param = strchr(param, ']'); + if (param) { + *param++ = '\0'; + } + else { + fprintf(stderr, "Malformed Ipv6 address!\n"); + exit(1); + } + } + } + char *colon = strchr(param, ':'); + char *comma = strchr(param, ','); + if (colon && (!comma || colon < comma)) { + *colon++ = '\0'; + *port = colon; + } + if (comma) { + *comma++ = '\0'; + return comma; + } + } + return NULL; +} + +uint32_t atouint32_metric(const char *str, const char *error_hint) +{ + if (!str) { + fprintf(stderr, "%smissing number argument\n", error_hint); + exit(1); + } + + if (!*str) { + fprintf(stderr, "%sempty number argument\n", error_hint); + exit(1); + } + + char *endptr; + double val = strtod(str, &endptr); + + if (str == endptr) { + fprintf(stderr, "%sinvalid number argument (%s)\n", error_hint, str); + exit(1); + } + + if (val < 0.0) { + fprintf(stderr, "%snon-negative number argument expected (%f)\n", error_hint, val); + exit(1); + } + + // allow whitespace before suffix + while (*endptr == ' ' || *endptr == '\t') + ++endptr; + + switch (*endptr) { + case '\0': + break; + case 'k': + case 'K': + val *= 1e3; + break; + case 'M': + case 'm': + val *= 1e6; + break; + case 'G': + case 'g': + val *= 1e9; + break; + default: + fprintf(stderr, "%sunknown number suffix (%s)\n", error_hint, endptr); + exit(1); + } + + if (val > UINT32_MAX) { + fprintf(stderr, "%snumber argument too big (%f)\n", error_hint, val); + exit(1); + } + + val += 1e-5; // rounding (e.g. 4123456789.99999) + if (val - (uint32_t)val > 2e-5) { + fprintf(stderr, "%sdecimal fraction (%f) did you forget k, M, or G suffix?\n", error_hint, val - (uint32_t)val); + } + + return (uint32_t)val; +} + +int atoi_time(const char *str, const char *error_hint) +{ + if (!str) { + fprintf(stderr, "%smissing time argument\n", error_hint); + exit(1); + } + + if (!*str) { + fprintf(stderr, "%sempty time argument\n", error_hint); + exit(1); + } + + char *endptr = NULL; + double val = 0.0; + unsigned colons = 0; + + do { + double num = strtod(str, &endptr); + + if (!endptr || str == endptr) { + fprintf(stderr, "%sinvalid time argument (%s)\n", error_hint, str); + exit(1); + } + + // allow whitespace before suffix + while (*endptr == ' ' || *endptr == '\t') + ++endptr; + + switch (*endptr) { + case '\0': + if (colons == 0) { + // assume seconds + val += num; + break; + } + // intentional fallthrough + case ':': + ++colons; + if (colons == 1) + val += num * 60 * 60; + else if (colons == 2) + val += num * 60; + else if (colons == 3) + val += num; + else { + fprintf(stderr, "%stoo many colons (use HH:MM[:SS]))\n", error_hint); + exit(1); + } + if (*endptr) + ++endptr; + break; + case 's': + case 'S': + val += num; + ++endptr; + break; + case 'm': + case 'M': + val += num * 60; + ++endptr; + break; + case 'h': + case 'H': + val += num * 60 * 60; + ++endptr; + break; + case 'd': + case 'D': + val += num * 60 * 60 * 24; + ++endptr; + break; + default: + fprintf(stderr, "%sunknown time suffix (%s)\n", error_hint, endptr); + exit(1); + } + + // chew up any remaining whitespace + while (*endptr == ' ' || *endptr == '\t') + ++endptr; + str = endptr; + + } while (*endptr); + + if (val > INT_MAX || val < INT_MIN) { + fprintf(stderr, "%stime argument too big (%f)\n", error_hint, val); + exit(1); + } + + if (val < 0) { + val -= 1e-5; // rounding (e.g. -4123456789.99999) + } + else { + val += 1e-5; // rounding (e.g. 4123456789.99999) + } + if (val - (int)(val) > 2e-5) { + fprintf(stderr, "%sdecimal fraction (%f) did you forget m, or h suffix?\n", error_hint, val - (uint32_t)val); + } + + return (int)val; +} + +char *asepc(char **stringp, char delim) +{ + if (!stringp || !*stringp) return NULL; + char *s = strchr(*stringp, delim); + if (s) *s++ = '\0'; + char *p = *stringp; + *stringp = s; + return p; +} + +char *getkwargs(char **s, char **key, char **val) +{ + char *v = asepc(s, ','); + char *k = asepc(&v, '='); + if (key) *key = k; + if (val) *val = v; + return k; +} + +char *trim_ws(char *str) +{ + if (!str || !*str) + return str; + while (*str == ' ' || *str == '\t' || *str == '\r' || *str == '\n') + ++str; + char *e = str; // end pointer (last non ws) + char *p = str; // scanning pointer + while (*p) { + while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') + ++p; + if (*p) + e = p++; + } + *++e = '\0'; + return str; +} + +char *remove_ws(char *str) +{ + if (!str) + return str; + char *d = str; // dst pointer + char *s = str; // src pointer + while (*s) { + while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n') + ++s; + if (*s) + *d++ = *s++; + } + *d++ = '\0'; + return str; +} + +// Unit testing +#ifdef _TEST +#define ASSERT_EQUALS(a,b) \ + do { \ + if ((a) == (b)) \ + ++passed; \ + else { \ + ++failed; \ + fprintf(stderr, "FAIL: %d <> %d\n", (a), (b)); \ + } \ + } while (0) + +int main(void) +{ + unsigned passed = 0; + unsigned failed = 0; + + fprintf(stderr, "optparse:: atouint32_metric\n"); + ASSERT_EQUALS(atouint32_metric("0", ""), 0); + ASSERT_EQUALS(atouint32_metric("1", ""), 1); + ASSERT_EQUALS(atouint32_metric("0.0", ""), 0); + ASSERT_EQUALS(atouint32_metric("1.0", ""), 1); + ASSERT_EQUALS(atouint32_metric("1.024k", ""), 1024); + ASSERT_EQUALS(atouint32_metric("433.92M", ""), 433920000); + ASSERT_EQUALS(atouint32_metric("433.94M", ""), 433940000); + ASSERT_EQUALS(atouint32_metric(" +1 G ", ""), 1000000000); + ASSERT_EQUALS(atouint32_metric("1e6", ""), 1000000); + + fprintf(stderr, "optparse:: atoi_time\n"); + ASSERT_EQUALS(atoi_time("0", ""), 0); + ASSERT_EQUALS(atoi_time("1", ""), 1); + ASSERT_EQUALS(atoi_time("0.0", ""), 0); + ASSERT_EQUALS(atoi_time("1.0", ""), 1); + ASSERT_EQUALS(atoi_time("1s", ""), 1); + ASSERT_EQUALS(atoi_time("2d", ""), 2 * 60 * 60 * 24); + ASSERT_EQUALS(atoi_time("2h", ""), 2 * 60 * 60); + ASSERT_EQUALS(atoi_time("2m", ""), 2 * 60); + ASSERT_EQUALS(atoi_time("2s", ""), 2); + ASSERT_EQUALS(atoi_time("2D", ""), 2 * 60 * 60 * 24); + ASSERT_EQUALS(atoi_time("2H", ""), 2 * 60 * 60); + ASSERT_EQUALS(atoi_time("2M", ""), 2 * 60); + ASSERT_EQUALS(atoi_time("2S", ""), 2); + ASSERT_EQUALS(atoi_time("2h3m4s", ""), 2 * 60 * 60 + 3 * 60 + 4); + ASSERT_EQUALS(atoi_time("2h 3m 4s", ""), 2 * 60 * 60 + 3 * 60 + 4); + ASSERT_EQUALS(atoi_time("2h3h 3m 4s 5", ""), 5 * 60 * 60 + 3 * 60 + 9); + ASSERT_EQUALS(atoi_time(" 2m ", ""), 2 * 60); + ASSERT_EQUALS(atoi_time("2 m", ""), 2 * 60); + ASSERT_EQUALS(atoi_time(" 2 m ", ""), 2 * 60); + ASSERT_EQUALS(atoi_time("-1m", ""), -60); + ASSERT_EQUALS(atoi_time("1h-15m", ""), 45 * 60); + + ASSERT_EQUALS(atoi_time("2:3", ""), 2 * 60 * 60 + 3 * 60); + ASSERT_EQUALS(atoi_time("2:3:4", ""), 2 * 60 * 60 + 3 * 60 + 4); + ASSERT_EQUALS(atoi_time("02:03", ""), 2 * 60 * 60 + 3 * 60); + ASSERT_EQUALS(atoi_time("02:03:04", ""), 2 * 60 * 60 + 3 * 60 + 4); + ASSERT_EQUALS(atoi_time(" 2 : 3 ", ""), 2 * 60 * 60 + 3 * 60); + ASSERT_EQUALS(atoi_time(" 2 : 3 : 4 ", ""), 2 * 60 * 60 + 3 * 60 + 4); + + fprintf(stderr, "optparse:: test (%u/%u) passed, (%u) failed.\n", passed, passed + failed, failed); + + return failed; +} +#endif /* _TEST */ diff --git a/src/optparse.h b/src/optparse.h new file mode 100644 index 0000000..6325693 --- /dev/null +++ b/src/optparse.h @@ -0,0 +1,105 @@ +/** @file + Option parsing functions to complement getopt. + + Copyright (C) 2017 Christian Zuckschwerdt + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. +*/ + +#ifndef INCLUDE_OPTPARSE_H_ +#define INCLUDE_OPTPARSE_H_ + +#include + +// makes strcasecmp() and strncasecmp() available when including optparse.h +#ifdef _MSC_VER + #include + #define strcasecmp(s1,s2) _stricmp(s1,s2) + #define strncasecmp(s1,s2,n) _strnicmp(s1,s2,n) +#else + #include +#endif + +/// Convert string to bool with fallback default. +/// Parses "true", "yes", "on", "enable" (not case-sensitive) to 1, atoi() otherwise. +int atobv(char *arg, int def); + +/// Convert string to int with fallback default. +int atoiv(char *arg, int def); + +/// Get the next colon or comma separated arg, NULL otherwise. +/// Returns string including comma if a comma is found first, +/// otherwise string after colon if found, NULL otherwise. +char *arg_param(char *arg); + +/// Convert a string with optional leading equals char to a double. +/// +/// Parse errors will fprintf(stderr, ...) and exit(1). +/// +/// @param str character string to parse +/// @param error_hint prepended to error output +/// @return parsed number value +double arg_float(const char *str, const char *error_hint); + +/// Parse param string to host and port. +/// E.g. ":514", "localhost", "[::1]", "127.0.0.1:514", "[::1]:514", +/// also "//localhost", "//localhost:514", "//:514". +/// Host or port are terminated at a comma, if found. +/// @return the remaining options +char *hostport_param(char *param, char **host, char **port); + +/// Convert a string to an unsigned integer, uses strtod() and accepts +/// metric suffixes of 'k', 'M', and 'G' (also 'K', 'm', and 'g'). +/// +/// Parse errors will fprintf(stderr, ...) and exit(1). +/// +/// @param str character string to parse +/// @param error_hint prepended to error output +/// @return parsed number value +uint32_t atouint32_metric(const char *str, const char *error_hint); + +/// Convert a string to an integer, uses strtod() and accepts +/// time suffixes of 'd', 'h', 'm', and 's' (also 'D', 'H', 'M', and 'S'), +/// or the form hours:minutes[:seconds]. +/// +/// Parse errors will fprintf(stderr, ...) and exit(1). +/// +/// @param str character string to parse +/// @param error_hint prepended to error output +/// @return parsed number value +int atoi_time(const char *str, const char *error_hint); + +/// Similar to strsep. +/// +/// @param[in,out] stringp String to parse inplace +/// @param delim the delimiter character +/// @return the original value of *stringp +char *asepc(char **stringp, char delim); + +/// Parse a comma-separated list of key/value pairs into kwargs. +/// +/// The input string will be modified and the pointer advanced. +/// The key and val pointers will be into the original string. +/// +/// @param[in,out] s String of key=value pairs, separated by commas +/// @param[out] key keyword argument if found, NULL otherwise +/// @param[out] val value if found, NULL otherwise +/// @return the original value of *stringp (the keyword found) +char *getkwargs(char **s, char **key, char **val); + +/// Trim left and right whitespace in string. +/// +/// @param[in,out] str String to change inplace +/// @return the trimmed value of str +char *trim_ws(char *str); + +/// Remove all whitespace from string. +/// +/// @param[in,out] str String to change inplace +/// @return the stripped value of str +char *remove_ws(char *str); + +#endif /* INCLUDE_OPTPARSE_H_ */