From 2e699d04ad816a401fb481a619e9a3b13dbb9a5e Mon Sep 17 00:00:00 2001 From: Keuin Date: Sat, 21 Jan 2023 01:31:05 +0800 Subject: Parse CLI arguments with `https://github.com/skeeto/optparse` --- CMakeLists.txt | 2 +- logging.h | 9 +- netmon.c | 105 +++++++++------ optparse.h | 395 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 469 insertions(+), 42 deletions(-) create mode 100644 optparse.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 450b881..e6a8246 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,4 +4,4 @@ project(netmon C) set(CMAKE_C_STANDARD 99) add_compile_definitions(DEBUG) -add_executable(netmon netmon.c logging.c logging.h netcheck.c netcheck.h validate.c validate.h) +add_executable(netmon netmon.c logging.c logging.h netcheck.c netcheck.h validate.c validate.h optparse.h) diff --git a/logging.h b/logging.h index 71e1407..a0f6f62 100644 --- a/logging.h +++ b/logging.h @@ -7,11 +7,18 @@ #include +#define die(args...) \ + do { \ + fprintf(stderr, args); \ + exit(1); \ + } while(0) + void *log_init(const char *filename); void log_free(void *logger); -void log_print(void *logger, const char *level, time_t ts, const char *filename, int lineno, const char *msg); +void log_print(void *logger, const char *level, time_t ts, const char *filename, + int lineno, const char *msg); #ifdef DEBUG #define log_debug(logger, msg) log_print((logger), "DEBUG", time(NULL), __FILE__, __LINE__, (msg)) diff --git a/netmon.c b/netmon.c index db9066d..38cf800 100644 --- a/netmon.c +++ b/netmon.c @@ -6,6 +6,10 @@ #include #include +#define OPTPARSE_IMPLEMENTATION +#define OPTPARSE_API static + +#include "optparse.h" const char *logfile = "netmon.log"; const char *pingdest = NULL; // which host to ping. If NULL, test tcp instead @@ -139,46 +143,67 @@ void loop() { } int main(int argc, char *argv[]) { - for (int i = 1; i < argc; ++i) { - if (!strcmp(argv[i], "-t")) { - check_interval_seconds = atoi(argv[i + 1]); - if (check_interval_seconds <= 0) { - fprintf(stderr, "check interval should be positive.\n"); - return -1; - } - ++i; // skip next value - } else if (!strcmp(argv[i], "-n")) { - max_check_failure = atoi(argv[i + 1]); - if (max_check_failure < 0) { - fprintf(stderr, "max check failure should not be negative.\n"); - return -1; - } - ++i; // skip next value - } else if (!strcmp(argv[i], "-l")) { - logfile = argv[i + 1]; - ++i; // skip next value - } else if (!strcmp(argv[i], "-p")) { - pingdest = argv[i + 1]; - ++i; // skip next value - } else if (!strcmp(argv[i], "-c")) { - char *s = argv[i + 1]; - unsigned long len = strlen(s); - if (len == 0) { - fprintf(stderr, "Empty fail command.\n"); - return -1; - } - failcmd = s; - fprintf(stderr, "Fail command is set to `%s`.\n", failcmd); - ++i; // skip next value - } else if (!strcmp(argv[i], "-d")) { - as_daemon = 1; - } else { - int invalid = strcmp(argv[i], "-h") != 0 && strcmp(argv[i], "--help") != 0; - if (invalid) printf("Unrecognized parameter \"%s\".\n", argv[i]); - printf("Usage:\n" - " %s [-t ] [-n ] [-l ] [-c ] [-p ] [-d]\n", - argv[0]); - return invalid != 0; + struct optparse_long opts[] = { + {"interval", 't', OPTPARSE_REQUIRED}, + {"max-failure", 'n', OPTPARSE_REQUIRED}, + {"log", 'l', OPTPARSE_REQUIRED}, + {"ping", 'p', OPTPARSE_REQUIRED}, + {"command", 'c', OPTPARSE_REQUIRED}, + {"daemon", 'd', OPTPARSE_NONE}, + {"help", 'h', OPTPARSE_NONE}, + {0} + }; + int option; + struct optparse options; + + char *end; + + optparse_init(&options, argv); + while ((option = optparse_long(&options, opts, NULL)) != -1) { + switch (option) { + case 't': + check_interval_seconds = (int) strtol(options.optarg, &end, 10); + if (*end != '\0') { + die("Invalid check interval: %s\n", options.optarg); + } + if (check_interval_seconds <= 0) { + die("Check interval should be positive.\n"); + } + break; + case 'n': + max_check_failure = (int) strtol(options.optarg, &end, 10); + if (*end != '\0') { + die("Invalid max check failure times: %s\n", + options.optarg); + } + if (max_check_failure < 0) { + die("Max check failure times should not be negative.\n"); + } + break; + case 'l': + logfile = strdup(options.optarg); + break; + case 'p': + pingdest = strdup(options.optarg); + break; + case 'c': + failcmd = strdup(options.optarg); + break; + case 'd': + as_daemon = 1; + break; + case 'h': + printf("Usage: %s " + "[-t ] " + "[-n ] " + "[-l ] " + "[-c ] " + "[-p ] " + "[-d]\n", + argv[0]); + exit(0); + default: + die("%s: %s\n", argv[0], options.errmsg); } } diff --git a/optparse.h b/optparse.h new file mode 100644 index 0000000..0219698 --- /dev/null +++ b/optparse.h @@ -0,0 +1,395 @@ +/** + * Copied from https://github.com/skeeto/optparse + * Commit cdc96b235343c8cf6475482075371b249b2187d6 + * https://github.com/skeeto/optparse/blob/master/optparse.h + * @author skeeto + */ +/* Optparse --- portable, reentrant, embeddable, getopt-like option parser + * + * This is free and unencumbered software released into the public domain. + * + * To get the implementation, define OPTPARSE_IMPLEMENTATION. + * Optionally define OPTPARSE_API to control the API's visibility + * and/or linkage (static, __attribute__, __declspec). + * + * The POSIX getopt() option parser has three fatal flaws. These flaws + * are solved by Optparse. + * + * 1) Parser state is stored entirely in global variables, some of + * which are static and inaccessible. This means only one thread can + * use getopt(). It also means it's not possible to recursively parse + * nested sub-arguments while in the middle of argument parsing. + * Optparse fixes this by storing all state on a local struct. + * + * 2) The POSIX standard provides no way to properly reset the parser. + * This means for portable code that getopt() is only good for one + * run, over one argv with one option string. It also means subcommand + * options cannot be processed with getopt(). Most implementations + * provide a method to reset the parser, but it's not portable. + * Optparse provides an optparse_arg() function for stepping over + * subcommands and continuing parsing of options with another option + * string. The Optparse struct itself can be passed around to + * subcommand handlers for additional subcommand option parsing. A + * full reset can be achieved by with an additional optparse_init(). + * + * 3) Error messages are printed to stderr. This can be disabled with + * opterr, but the messages themselves are still inaccessible. + * Optparse solves this by writing an error message in its errmsg + * field. The downside to Optparse is that this error message will + * always be in English rather than the current locale. + * + * Optparse should be familiar with anyone accustomed to getopt(), and + * it could be a nearly drop-in replacement. The option string is the + * same and the fields have the same names as the getopt() global + * variables (optarg, optind, optopt). + * + * Optparse also supports GNU-style long options with optparse_long(). + * The interface is slightly different and simpler than getopt_long(). + * + * By default, argv is permuted as it is parsed, moving non-option + * arguments to the end. This can be disabled by setting the `permute` + * field to 0 after initialization. + */ +#ifndef OPTPARSE_H +#define OPTPARSE_H + +#ifndef OPTPARSE_API +# define OPTPARSE_API +#endif + +struct optparse { + char **argv; + int permute; + int optind; + int optopt; + char *optarg; + char errmsg[64]; + int subopt; +}; + +enum optparse_argtype { + OPTPARSE_NONE, + OPTPARSE_REQUIRED, + OPTPARSE_OPTIONAL +}; + +struct optparse_long { + const char *longname; + int shortname; + enum optparse_argtype argtype; +}; + +/** + * Initializes the parser state. + */ +OPTPARSE_API +void optparse_init(struct optparse *options, char **argv); + +/** + * Read the next option in the argv array. + * @param optstring a getopt()-formatted option string. + * @return the next option character, -1 for done, or '?' for error + * + * Just like getopt(), a character followed by no colons means no + * argument. One colon means the option has a required argument. Two + * colons means the option takes an optional argument. + */ +OPTPARSE_API +int optparse(struct optparse *options, const char *optstring); + +/** + * Handles GNU-style long options in addition to getopt() options. + * This works a lot like GNU's getopt_long(). The last option in + * longopts must be all zeros, marking the end of the array. The + * longindex argument may be NULL. + */ +OPTPARSE_API +int optparse_long(struct optparse *options, + const struct optparse_long *longopts, + int *longindex); + +/** + * Used for stepping over non-option arguments. + * @return the next non-option argument, or NULL for no more arguments + * + * Argument parsing can continue with optparse() after using this + * function. That would be used to parse the options for the + * subcommand returned by optparse_arg(). This function allows you to + * ignore the value of optind. + */ +OPTPARSE_API +char *optparse_arg(struct optparse *options); + +/* Implementation */ +#ifdef OPTPARSE_IMPLEMENTATION + +#define OPTPARSE_MSG_INVALID "invalid option" +#define OPTPARSE_MSG_MISSING "option requires an argument" +#define OPTPARSE_MSG_TOOMANY "option takes no arguments" + +static int +optparse_error(struct optparse *options, const char *msg, const char *data) { + unsigned p = 0; + const char *sep = " -- '"; + while (*msg) + options->errmsg[p++] = *msg++; + while (*sep) + options->errmsg[p++] = *sep++; + while (p < sizeof(options->errmsg) - 2 && *data) + options->errmsg[p++] = *data++; + options->errmsg[p++] = '\''; + options->errmsg[p++] = '\0'; + return '?'; +} + +OPTPARSE_API +void +optparse_init(struct optparse *options, char **argv) { + options->argv = argv; + options->permute = 1; + options->optind = argv[0] != 0; + options->subopt = 0; + options->optarg = 0; + options->errmsg[0] = '\0'; +} + +static int +optparse_is_dashdash(const char *arg) { + return arg != 0 && arg[0] == '-' && arg[1] == '-' && arg[2] == '\0'; +} + +static int +optparse_is_shortopt(const char *arg) { + return arg != 0 && arg[0] == '-' && arg[1] != '-' && arg[1] != '\0'; +} + +static int +optparse_is_longopt(const char *arg) { + return arg != 0 && arg[0] == '-' && arg[1] == '-' && arg[2] != '\0'; +} + +static void +optparse_permute(struct optparse *options, int index) { + char *nonoption = options->argv[index]; + int i; + for (i = index; i < options->optind - 1; i++) + options->argv[i] = options->argv[i + 1]; + options->argv[options->optind - 1] = nonoption; +} + +static int +optparse_argtype(const char *optstring, char c) { + int count = OPTPARSE_NONE; + if (c == ':') + return -1; + for (; *optstring && c != *optstring; optstring++); + if (!*optstring) + return -1; + if (optstring[1] == ':') + count += optstring[2] == ':' ? 2 : 1; + return count; +} + +OPTPARSE_API +int +optparse(struct optparse *options, const char *optstring) { + int type; + char *next; + char *option = options->argv[options->optind]; + options->errmsg[0] = '\0'; + options->optopt = 0; + options->optarg = 0; + if (option == 0) { + return -1; + } else if (optparse_is_dashdash(option)) { + options->optind++; /* consume "--" */ + return -1; + } else if (!optparse_is_shortopt(option)) { + if (options->permute) { + int index = options->optind++; + int r = optparse(options, optstring); + optparse_permute(options, index); + options->optind--; + return r; + } else { + return -1; + } + } + option += options->subopt + 1; + options->optopt = option[0]; + type = optparse_argtype(optstring, option[0]); + next = options->argv[options->optind + 1]; + switch (type) { + case -1: { + char str[2] = {0, 0}; + str[0] = option[0]; + options->optind++; + return optparse_error(options, OPTPARSE_MSG_INVALID, str); + } + case OPTPARSE_NONE: + if (option[1]) { + options->subopt++; + } else { + options->subopt = 0; + options->optind++; + } + return option[0]; + case OPTPARSE_REQUIRED: + options->subopt = 0; + options->optind++; + if (option[1]) { + options->optarg = option + 1; + } else if (next != 0) { + options->optarg = next; + options->optind++; + } else { + char str[2] = {0, 0}; + str[0] = option[0]; + options->optarg = 0; + return optparse_error(options, OPTPARSE_MSG_MISSING, str); + } + return option[0]; + case OPTPARSE_OPTIONAL: + options->subopt = 0; + options->optind++; + if (option[1]) + options->optarg = option + 1; + else + options->optarg = 0; + return option[0]; + } + return 0; +} + +OPTPARSE_API +char * +optparse_arg(struct optparse *options) { + char *option = options->argv[options->optind]; + options->subopt = 0; + if (option != 0) + options->optind++; + return option; +} + +static int +optparse_longopts_end(const struct optparse_long *longopts, int i) { + return !longopts[i].longname && !longopts[i].shortname; +} + +static void +optparse_from_long(const struct optparse_long *longopts, char *optstring) { + char *p = optstring; + int i; + for (i = 0; !optparse_longopts_end(longopts, i); i++) { + if (longopts[i].shortname && longopts[i].shortname < 127) { + int a; + *p++ = longopts[i].shortname; + for (a = 0; a < (int) longopts[i].argtype; a++) + *p++ = ':'; + } + } + *p = '\0'; +} + +/* Unlike strcmp(), handles options containing "=". */ +static int +optparse_longopts_match(const char *longname, const char *option) { + const char *a = option, *n = longname; + if (longname == 0) + return 0; + for (; *a && *n && *a != '='; a++, n++) + if (*a != *n) + return 0; + return *n == '\0' && (*a == '\0' || *a == '='); +} + +/* Return the part after "=", or NULL. */ +static char * +optparse_longopts_arg(char *option) { + for (; *option && *option != '='; option++); + if (*option == '=') + return option + 1; + else + return 0; +} + +static int +optparse_long_fallback(struct optparse *options, + const struct optparse_long *longopts, + int *longindex) { + int result; + char optstring[96 * 3 + 1]; /* 96 ASCII printable characters */ + optparse_from_long(longopts, optstring); + result = optparse(options, optstring); + if (longindex != 0) { + *longindex = -1; + if (result != -1) { + int i; + for (i = 0; !optparse_longopts_end(longopts, i); i++) + if (longopts[i].shortname == options->optopt) + *longindex = i; + } + } + return result; +} + +OPTPARSE_API +int +optparse_long(struct optparse *options, + const struct optparse_long *longopts, + int *longindex) { + int i; + char *option = options->argv[options->optind]; + if (option == 0) { + return -1; + } else if (optparse_is_dashdash(option)) { + options->optind++; /* consume "--" */ + return -1; + } else if (optparse_is_shortopt(option)) { + return optparse_long_fallback(options, longopts, longindex); + } else if (!optparse_is_longopt(option)) { + if (options->permute) { + int index = options->optind++; + int r = optparse_long(options, longopts, longindex); + optparse_permute(options, index); + options->optind--; + return r; + } else { + return -1; + } + } + + /* Parse as long option. */ + options->errmsg[0] = '\0'; + options->optopt = 0; + options->optarg = 0; + option += 2; /* skip "--" */ + options->optind++; + for (i = 0; !optparse_longopts_end(longopts, i); i++) { + const char *name = longopts[i].longname; + if (optparse_longopts_match(name, option)) { + char *arg; + if (longindex) + *longindex = i; + options->optopt = longopts[i].shortname; + arg = optparse_longopts_arg(option); + if (longopts[i].argtype == OPTPARSE_NONE && arg != 0) { + return optparse_error(options, OPTPARSE_MSG_TOOMANY, name); + } + if (arg != 0) { + options->optarg = arg; + } else if (longopts[i].argtype == OPTPARSE_REQUIRED) { + options->optarg = options->argv[options->optind]; + if (options->optarg == 0) + return optparse_error(options, OPTPARSE_MSG_MISSING, name); + else + options->optind++; + } + return options->optopt; + } + } + return optparse_error(options, OPTPARSE_MSG_INVALID, option); +} + +#endif /* OPTPARSE_IMPLEMENTATION */ +#endif /* OPTPARSE_H */ \ No newline at end of file -- cgit v1.2.3