From 216f312578be5c959527c03ccc70d27eebcc76aa Mon Sep 17 00:00:00 2001 From: NRK Date: Tue, 16 Aug 2022 10:43:50 +0200 Subject: [PATCH] add support for long-opts (#332) Uses [optparse] to add support for long-opts. optparse is posix compliant with getopt(3) and thus would be backwards compatible. It does not have any dependency (not even the c standard library!) and is C89 compatible and thus fits our current code-style. [optparse]: https://github.com/skeeto/optparse Note that we're using a couple `pragma`-s to silence some harmless warnings. This should be portable because these pragma-s don't change the behavior of the program. Furthermore, C standard mandates that unknown pragma's should be ignored by the compiler and thus would not result in build failure on compilers which do not recognize them. Closes: https://codeberg.org/nsxiv/nsxiv/issues/328 Reviewed-on: https://codeberg.org/nsxiv/nsxiv/pulls/332 Reviewed-by: eylles --- Makefile | 2 +- etc/nsxiv.1 | 56 ++++---- options.c | 92 ++++++++---- optparse.h | 403 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 500 insertions(+), 53 deletions(-) create mode 100644 optparse.h diff --git a/Makefile b/Makefile index 04d2ed7..3527741 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ nsxiv: $(objs) $(CC) $(CFLAGS) $(nsxiv_cppflags) -c -o $@ $< $(objs): Makefile config.mk nsxiv.h config.h commands.h -options.o: version.h +options.o: version.h optparse.h window.o: icon/data.h utf8.h config.h: diff --git a/etc/nsxiv.1 b/etc/nsxiv.1 index f4801b0..29f036f 100644 --- a/etc/nsxiv.1 +++ b/etc/nsxiv.1 @@ -34,84 +34,84 @@ Please note, that the fullscreen mode requires an EWMH/NetWM-compliant window manager. .SH OPTIONS .TP -.BI "\-A " FRAMERATE +.BI "\-A, \-\-framerate " FRAMERATE Play animations with a constant frame rate set to .IR FRAMERATE . .TP -.B \-a +.B "\-a, \-\-animate" Play animations of multi-frame images. .TP -.B \-b +.B "\-b, \-\-no\-bar" Do not show statusbar at the bottom of the window. .TP -.B \-c +.B "\-c, \-\-clean\-cache" Remove all orphaned cache files from the thumbnail cache directory and exit. .TP -.BI "\-e " WID +.BI "\-e, \-\-embed " WID Embed nsxiv's window into window whose ID is .IR WID . .TP -.B \-f +.B "\-f, \-\-fullscreen" Start in fullscreen mode. .TP -.BI "\-G " GAMMA +.BI "\-G, \-\-gamma " GAMMA Set image gamma to GAMMA (\-32..32). .TP -.BI "\-g " GEOMETRY +.BI "\-g, \-\-geometry " GEOMETRY Set window position and size. See section GEOMETRY SPECIFICATIONS of X(7) for more information on GEOMETRY argument. .TP -.BI "\-N " NAME -Set the resource name of nsxiv's X window to NAME. -.TP -.BI "\-n " NUM -Start at picture number NUM. -.TP -.B \-h +.B "\-h, \-\-help" Print brief usage information to standard output and exit. .TP -.B \-i +.B "\-i, \-\-stdin" Read names of files to open from standard input. Also done if FILE is `-'. .TP -.B \-o +.BI "\-N, \-\-class " NAME +Set the resource name (WM_CLASS) of nsxiv's X window to NAME. +.TP +.BI "\-n, \-\-start\-at " NUM +Start at picture number NUM. +.TP +.B "\-o, \-\-stdout" Write list of all marked files to standard output when quitting. In combination with -.B \-i +.B "\-i, \-\-stdin" nsxiv can be used as a visual filter/pipe. .TP -.B \-p +.B "\-p, \-\-private" Enable private mode, in which nsxiv does not write any cache or temporary files. .TP -.B \-q +.B "\-q, \-\-quiet" Be quiet, and disable warnings to standard error stream. .TP -.B \-r +.B "\-r, \-\-recursive" Search the given directories recursively for images to view. .TP -.BI "\-S " DELAY +.BI "\-S, \-\-ss\-delay " DELAY Start in slideshow mode. Set the delay between images to .I DELAY seconds. .I DELAY may be a floating-point number. .TP -.BI "\-s " MODE +.BI "\-s, \-\-scale\-mode " MODE Set scale mode according to MODE character. Supported modes are: [d]own, [f]it, [F]ill, [w]idth, [h]eight. .TP -.B \-t +.B "\-t, \-\-thumbnail" Start in thumbnail mode. .TP -.B \-v +.B "\-v, \-\-version" Print version information to standard output and exit. .TP -.B \-Z +.B "\-Z, \-\-zoom\-100" The same as `\-z 100'. .TP -.BI "\-z " ZOOM +.BI "\-z, \-\-zoom " ZOOM Set zoom level to ZOOM percent. .TP -.B \-0 +.B "\-0, \-\-null" Use NULL-separator. With this option, output of \-o and file-list sent to the key-handler and the input of \-i will be separated by a NULL character. .SH KEYBOARD COMMANDS diff --git a/options.c b/options.c index 69b45b9..3b191ea 100644 --- a/options.c +++ b/options.c @@ -20,11 +20,20 @@ #include "nsxiv.h" #include "version.h" +#include #include #include #include #include +#define OPTPARSE_IMPLEMENTATION +#define OPTPARSE_API static +#pragma GCC diagnostic push /* also works on clang */ +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wunused-function" +#include "optparse.h" +#pragma GCC diagnostic pop + const opt_t *options; void print_usage(void) @@ -54,12 +63,41 @@ static void print_version(void) void parse_options(int argc, char **argv) { + static const struct optparse_long longopts[] = { + { "framerate", 'A', OPTPARSE_REQUIRED }, + { "animate", 'a', OPTPARSE_NONE }, + { "no-bar", 'b', OPTPARSE_NONE }, + { "clean-cache", 'c', OPTPARSE_NONE }, + { "embed", 'e', OPTPARSE_REQUIRED }, + { "fullscreen", 'f', OPTPARSE_NONE }, + { "gamma", 'G', OPTPARSE_REQUIRED }, + { "geometry", 'g', OPTPARSE_REQUIRED }, + { "help", 'h', OPTPARSE_NONE }, + { "stdin", 'i', OPTPARSE_NONE }, + { "class", 'N', OPTPARSE_REQUIRED }, + { "start-at", 'n', OPTPARSE_REQUIRED }, + { "stdout", 'o', OPTPARSE_NONE }, + { "private", 'p', OPTPARSE_NONE }, + { "quiet", 'q', OPTPARSE_NONE }, + { "recursive", 'r', OPTPARSE_NONE }, + { "ss-delay", 'S', OPTPARSE_REQUIRED }, + { "scale-mode", 's', OPTPARSE_REQUIRED }, + { NULL, 'T', OPTPARSE_REQUIRED }, + { "thumbnail", 't', OPTPARSE_NONE }, + { "version", 'v', OPTPARSE_NONE }, + { "zoom-100", 'Z', OPTPARSE_NONE }, + { "zoom", 'z', OPTPARSE_REQUIRED }, + { "null", '0', OPTPARSE_NONE }, + { 0 }, /* end */ + }; + int n, opt; char *end, *s; - const char *scalemodes = "dfFwh"; + struct optparse op; + const char scalemodes[] = "dfFwh"; /* must be sorted according to scalemode_t */ static opt_t _options; - options = &_options; + options = &_options; progname = strrchr(argv[0], '/'); progname = progname ? progname + 1 : argv[0]; @@ -87,15 +125,21 @@ void parse_options(int argc, char **argv) _options.clean_cache = false; _options.private_mode = false; - while ((opt = getopt(argc, argv, "A:abce:fG:g:hin:N:opqrS:s:T:tvZz:0")) != -1) { + optparse_init(&op, argv); + while ((opt = optparse_long(&op, longopts, NULL)) != -1) { + for (n = 0; n < (int)ARRLEN(longopts); ++n) { /* clang-tidy finds some non-sensical branch and thinks optarg == NULL is possible */ + if (opt == longopts[n].shortname && longopts[n].argtype == OPTPARSE_REQUIRED) + assert(op.optarg != NULL); + } switch (opt) { case '?': + fprintf(stderr, "%s\n", op.errmsg); print_usage(); exit(EXIT_FAILURE); case 'A': - n = strtol(optarg, &end, 0); + n = strtol(op.optarg, &end, 0); if (*end != '\0' || n <= 0) - error(EXIT_FAILURE, 0, "Invalid argument for option -A: %s", optarg); + error(EXIT_FAILURE, 0, "Invalid argument for option -A: %s", op.optarg); _options.framerate = n; /* fall through */ case 'a': @@ -108,22 +152,22 @@ void parse_options(int argc, char **argv) _options.clean_cache = true; break; case 'e': - n = strtol(optarg, &end, 0); + n = strtol(op.optarg, &end, 0); if (*end != '\0') - error(EXIT_FAILURE, 0, "Invalid argument for option -e: %s", optarg); + error(EXIT_FAILURE, 0, "Invalid argument for option -e: %s", op.optarg); _options.embed = n; break; case 'f': _options.fullscreen = true; break; case 'G': - n = strtol(optarg, &end, 0); + n = strtol(op.optarg, &end, 0); if (*end != '\0') - error(EXIT_FAILURE, 0, "Invalid argument for option -G: %s", optarg); + error(EXIT_FAILURE, 0, "Invalid argument for option -G: %s", op.optarg); _options.gamma = n; break; case 'g': - _options.geometry = optarg; + _options.geometry = op.optarg; break; case 'h': print_usage(); @@ -132,13 +176,13 @@ void parse_options(int argc, char **argv) _options.from_stdin = true; break; case 'n': - n = strtol(optarg, &end, 0); + n = strtol(op.optarg, &end, 0); if (*end != '\0' || n <= 0) - error(EXIT_FAILURE, 0, "Invalid argument for option -n: %s", optarg); + error(EXIT_FAILURE, 0, "Invalid argument for option -n: %s", op.optarg); _options.startnum = n - 1; break; case 'N': - _options.res_name = optarg; + _options.res_name = op.optarg; break; case 'o': _options.to_stdout = true; @@ -153,15 +197,15 @@ void parse_options(int argc, char **argv) _options.recursive = true; break; case 'S': - n = strtof(optarg, &end) * 10; + n = strtof(op.optarg, &end) * 10; if (*end != '\0' || n <= 0) - error(EXIT_FAILURE, 0, "Invalid argument for option -S: %s", optarg); + error(EXIT_FAILURE, 0, "Invalid argument for option -S: %s", op.optarg); _options.slideshow = n; break; case 's': - s = strchr(scalemodes, optarg[0]); - if (s == NULL || *s == '\0' || strlen(optarg) != 1) - error(EXIT_FAILURE, 0, "Invalid argument for option -s: %s", optarg); + s = strchr(scalemodes, op.optarg[0]); + if (s == NULL || *s == '\0' || strlen(op.optarg) != 1) + error(EXIT_FAILURE, 0, "Invalid argument for option -s: %s", op.optarg); _options.scalemode = s - scalemodes; break; case 'T': @@ -175,14 +219,14 @@ void parse_options(int argc, char **argv) exit(EXIT_SUCCESS); case 'Z': _options.scalemode = SCALE_ZOOM; - _options.zoom = 1.0; + _options.zoom = 1.0f; break; case 'z': - n = strtol(optarg, &end, 0); + n = strtol(op.optarg, &end, 0); if (*end != '\0' || n <= 0) - error(EXIT_FAILURE, 0, "Invalid argument for option -z: %s", optarg); + error(EXIT_FAILURE, 0, "Invalid argument for option -z: %s", op.optarg); _options.scalemode = SCALE_ZOOM; - _options.zoom = (float) n / 100.0; + _options.zoom = (float) n / 100.0f; break; case '0': _options.using_null = true; @@ -190,8 +234,8 @@ void parse_options(int argc, char **argv) } } - _options.filenames = argv + optind; - _options.filecnt = argc - optind; + _options.filenames = argv + op.optind; + _options.filecnt = argc - op.optind; if (_options.filecnt == 1 && STREQ(_options.filenames[0], "-")) { _options.filenames++; diff --git a/optparse.h b/optparse.h new file mode 100644 index 0000000..8d6c0a9 --- /dev/null +++ b/optparse.h @@ -0,0 +1,403 @@ +/* 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 */