Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

[PATCH] zparseopts: add options -v (argv) and -G (GNU-style parsing)



these have been sitting in a branch for several years now, idr why i
didn't submit before

-v allows you to set the input array, in case you want to parse options
out of something besides $argv

-G enables gnu-style parsing of long options with optargs. specifically
it handles the --foo=bar syntax the way getopt_long(3) does. the lack of
this functionality has been an occasional complaint on-line for a long
time. for legacy's sake, singly hyphenated long options like -foo are
still handled the same as short ones, but i can change those too if
people want

dana


diff --git a/Doc/Zsh/mod_zutil.yo b/Doc/Zsh/mod_zutil.yo
index 9946618d6..80ce78ff0 100644
--- a/Doc/Zsh/mod_zutil.yo
+++ b/Doc/Zsh/mod_zutil.yo
@@ -228,7 +228,7 @@ item(tt(zregexparse))(
 This implements some internals of the tt(_regex_arguments) function.
 )
 findex(zparseopts)
-item(tt(zparseopts) [ tt(-D) tt(-E) tt(-F) tt(-K) tt(-M) ] [ tt(-a) var(array) ] [ tt(-A) var(assoc) ] [ tt(-) ] var(spec) ...)(
+item(tt(zparseopts) [ tt(-D) tt(-E) tt(-F) tt(-K) tt(-M) ] [ tt(-a) var(array) ] [ tt(-A) var(assoc) ] [ tt(-v) var(argv) ] [ tt(-) ] var(spec) ...)(
 This builtin simplifies the parsing of options in positional parameters,
 i.e. the set of arguments given by tt($*).  Each var(spec) describes one
 option and must be of the form `var(opt)[tt(=)var(array)]'.  If an option
@@ -355,6 +355,11 @@ is found, the values are stored as usual.  This changes only the way the
 values are stored, not the way tt($*) is parsed, so results may be
 unpredictable if the `var(name)tt(+)' specifier is used inconsistently.
 )
+item(tt(-v) var(argv))(
+With this option, the elements of the named array var(argv) are used as the
+positional parameters to parse, rather than those given by tt($*).  The
+array may be modified according to the tt(-D) option.
+)
 enditem()
 
 For example,
diff --git a/Src/Modules/zutil.c b/Src/Modules/zutil.c
index 5eccea7a9..fa9144a84 100644
--- a/Src/Modules/zutil.c
+++ b/Src/Modules/zutil.c
@@ -1713,6 +1713,7 @@ static int
 bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
 {
     char *o, *p, *n, **pp, **aval, **ap, *assoc = NULL, **cp, **np;
+    char *paramsname = NULL, **params;
     int del = 0, flags = 0, extract = 0, fail = 0, keep = 0;
     Zoptdesc sopts[256], d;
     Zoptarr a, defarr = NULL;
@@ -1808,6 +1809,20 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
 		    return 1;
 		}
 		break;
+	    case 'v':
+		if (paramsname) {
+		    zwarnnam(nam, "argv array given more than once");
+		    return 1;
+		}
+		if (o[2])
+		    paramsname = o + 2;
+		else if (*args)
+		    paramsname = *args++;
+		else {
+		    zwarnnam(nam, "missing array name");
+		    return 1;
+		}
+		break;
 	    default:
 		/* Anything else is an option description */
 		args--;
@@ -1901,7 +1916,8 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
 	    return 1;
 	}
     }
-    np = cp = pp = ((extract && del) ? arrdup(pparams) : pparams);
+    params = getaparam((paramsname = paramsname ? paramsname : "argv"));
+    np = cp = pp = ((extract && del) ? arrdup(params) : params);
     for (; (o = *pp); pp++) {
 	if (*o != '-') {
 	    if (extract) {
@@ -2041,12 +2057,9 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
     if (del) {
 	if (extract) {
 	    *cp = NULL;
-	    freearray(pparams);
-	    pparams = zarrdup(np);
+	    setaparam(paramsname, zarrdup(np));
 	} else {
-	    pp = zarrdup(pp);
-	    freearray(pparams);
-	    pparams = pp;
+	    setaparam(paramsname, zarrdup(pp));
 	}
     }
     return 0;
diff --git a/Test/V12zparseopts.ztst b/Test/V12zparseopts.ztst
index 816e1d041..fa82a9d0b 100644
--- a/Test/V12zparseopts.ztst
+++ b/Test/V12zparseopts.ztst
@@ -103,6 +103,14 @@
 0:zparseopts -M
 >ret: 0, optv: --aaa bar, opts: --aaa bar, argv: 1 2 3
 
+  () {
+    local -a optv argvv=( -a -b -c 1 2 3 )
+    zparseopts -D -a optv -v argvv - a b c
+    print -r - ret: $?, optv: $optv, argvv: $argvv, argv: $argv
+  } -x -y -z 7 8 9
+0:zparseopts -v
+>ret: 0, optv: -a -b -c, argvv: 1 2 3, argv: -x -y -z 7 8 9
+
   () {
     local -a optv aa ab
     zparseopts -a optv - a=aa b:=ab c:- z


diff --git a/Doc/Zsh/mod_zutil.yo b/Doc/Zsh/mod_zutil.yo
index 80ce78ff0..ef18d4d4c 100644
--- a/Doc/Zsh/mod_zutil.yo
+++ b/Doc/Zsh/mod_zutil.yo
@@ -228,7 +228,7 @@ item(tt(zregexparse))(
 This implements some internals of the tt(_regex_arguments) function.
 )
 findex(zparseopts)
-item(tt(zparseopts) [ tt(-D) tt(-E) tt(-F) tt(-K) tt(-M) ] [ tt(-a) var(array) ] [ tt(-A) var(assoc) ] [ tt(-v) var(argv) ] [ tt(-) ] var(spec) ...)(
+item(tt(zparseopts) [ tt(-D) tt(-E) tt(-F) tt(-G) tt(-K) tt(-M) ] [ tt(-a) var(array) ] [ tt(-A) var(assoc) ] [ tt(-v) var(argv) ] [ tt(-) ] var(spec) ...)(
 This builtin simplifies the parsing of options in positional parameters,
 i.e. the set of arguments given by tt($*).  Each var(spec) describes one
 option and must be of the form `var(opt)[tt(=)var(array)]'.  If an option
@@ -282,19 +282,19 @@ first colon.
 )
 enditem()
 
-In all cases, option-arguments must appear either immediately following the
+By default, option-arguments may appear either immediately following the
 option in the same positional parameter or in the next one. Even an optional
 argument may appear in the next parameter, unless it begins with a `tt(-)'.
-There is no special handling of `tt(=)' as with GNU-style argument parsers;
-given the var(spec) `tt(-foo:)', the positional parameter `tt(-)tt(-foo=bar)'
-is parsed as `tt(-)tt(-foo)' with an argument of `tt(=bar)'.
+Additionally, there is no special consideration given to an `tt(=)'
+between an option and its argument. To make parsing use the more common
+GNU-style conventions, use the tt(-G) option.
 
 When the names of two options that take no arguments overlap, the longest one
 wins, so that parsing for the var(spec)s `tt(-foo -foobar)' (for example) is
 unambiguous. However, due to the aforementioned handling of option-arguments,
 ambiguities may arise when at least one overlapping var(spec) takes an
 argument, as in `tt(-foo: -foobar)'. In that case, the last matching
-var(spec) wins.
+var(spec) wins. (This is not an issue with GNU-style parsing.)
 
 The options of tt(zparseopts) itself cannot be stacked because, for
 example, the stack `tt(-DEK)' is indistinguishable from a var(spec) for
@@ -338,6 +338,27 @@ Note that the appearance in the positional parameters of an option without
 its required argument always aborts parsing and returns an error as described
 above regardless of whether this option is used.
 )
+item(tt(-G))(
+This option specifies that doubly hyphenated long options that take an
+argument are parsed in the GNU style. That is, given the var(spec)
+`tt(-foo:)', the positional parameter `tt(-)tt(-foo=bar)' is interpreted
+as option `tt(-)tt(-foo)' with argument `tt(bar)', rather than argument
+`tt(=bar)' as is the default, whilst the positional parameter
+`tt(-)tt(-foobar)' is considered an unknown option (unless another
+var(spec) describes it).
+
+When this option is in effect, it is possible to supply an empty
+option-argument in the same parameter as its option using a trailing
+`tt(=)'. An optional argument to a long option must be given with the
+equals syntax; the following parameter is considered unrelated. Lastly,
+whenever a long option and its argument are stored in the same array
+element, an `tt(=)' is used to separate them.
+
+A mandatory option-argument given in a separate parameter from its
+option (as in `tt(-)tt(-foo) tt(bar)'), or any option-argument given to
+a singly hyphenated (short) option, is always treated the same
+regardless of whether this option is in effect.
+)
 item(tt(-K))(
 With this option, the arrays specified with the tt(-a) option and with the
 `tt(=)var(array)' forms are kept unchanged when none of the var(spec)s for
diff --git a/Src/Modules/zutil.c b/Src/Modules/zutil.c
index fa9144a84..a1bd63762 100644
--- a/Src/Modules/zutil.c
+++ b/Src/Modules/zutil.c
@@ -1534,6 +1534,7 @@ struct zoptdesc {
 #define ZOF_SAME 8
 #define ZOF_MAP 16
 #define ZOF_CYC 32
+#define ZOF_GNU 64
 
 struct zoptarr {
     Zoptarr next;
@@ -1568,9 +1569,29 @@ static Zoptdesc
 lookup_opt(char *str)
 {
     Zoptdesc p;
-
     for (p = opt_descs; p; p = p->next) {
-	if ((p->flags & ZOF_ARG) ? strpfx(p->name, str) : !strcmp(p->name, str))
+	/*
+	 * Option takes argument, with GNU-style handling of =. This should only
+	 * be set for doubly hyphenated long options, though we don't care about
+	 * that here. Unlike the default behaviour, matching is unambiguous
+	 */
+	if (p->flags & ZOF_GNU) {
+	    if (!strcmp(p->name, str) || /* Inefficient, whatever */
+		    (strpfx(p->name, str) && str[strlen(p->name)] == '='))
+		return p;
+	/*
+	 * Option takes argument, default 'cuddled' style. This is weird with
+	 * long options because we naively accept whatever option has a prefix
+	 * match for the given parameter. Thus if you have specs `-foo:` and
+	 * `-foobar:` (or even `-foobar` with no optarg), without the GNU
+	 * setting, the behaviour depends on the order in which they were
+	 * defined. It is documented to work this way though
+	 */
+	} else if (p->flags & ZOF_ARG) {
+	    if (strpfx(p->name, str))
+		return p;
+	/* Option takes no argument */
+	} else if (!strcmp(p->name, str))
 	    return p;
     }
     return NULL;
@@ -1641,10 +1662,13 @@ add_opt_val(Zoptdesc d, char *arg)
 	if (d->arr)
 	    d->arr->num += (arg ? 2 : 1);
     } else if (arg) {
-	char *s = (char *) zhalloc(strlen(d->name) + strlen(arg) + 2);
+	/* 3 here is '-' + '=' + NUL */
+	char *s = (char *) zhalloc(strlen(d->name) + strlen(arg) + 3);
 
 	*s = '-';
 	strcpy(s + 1, d->name);
+	if (d->flags & ZOF_GNU)
+	    strcat(s, "=");
 	strcat(s, arg);
 	v->str = s;
 	if (d->arr)
@@ -1714,7 +1738,7 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
 {
     char *o, *p, *n, **pp, **aval, **ap, *assoc = NULL, **cp, **np;
     char *paramsname = NULL, **params;
-    int del = 0, flags = 0, extract = 0, fail = 0, keep = 0;
+    int del = 0, flags = 0, extract = 0, fail = 0, gnu = 0, keep = 0;
     Zoptdesc sopts[256], d;
     Zoptarr a, defarr = NULL;
     Zoptval v;
@@ -1759,6 +1783,14 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
 		}
 		fail = 1;
 		break;
+	    case 'G':
+		if (o[2]) {
+		    args--;
+		    o = NULL;
+		    break;
+		}
+		gnu = 1;
+		break;
 	    case 'K':
 		if (o[2]) {
 		    args--;
@@ -1864,6 +1896,10 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
 	if (*p == ':') {
 	    f |= ZOF_ARG;
 	    *p = '\0';
+	    /* GNU setting applies only to doubly hyphenated options */
+	    if (*o == '-' && gnu) {
+		f |= ZOF_GNU;
+	    }
 	    if (*++p == ':') {
 		p++;
 		f |= ZOF_OPT;
@@ -1919,6 +1955,7 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
     params = getaparam((paramsname = paramsname ? paramsname : "argv"));
     np = cp = pp = ((extract && del) ? arrdup(params) : params);
     for (; (o = *pp); pp++) {
+	/* Not an option */
 	if (*o != '-') {
 	    if (extract) {
 		if (del)
@@ -1927,6 +1964,7 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
 	    } else
 		break;
 	}
+	/* '-' or '--', end parsing */
 	if (!o[1] || (o[1] == '-' && !o[2])) {
 	    if (del && extract)
 		*cp++ = o;
@@ -1934,6 +1972,7 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
 	    break;
 	}
 	if (!(d = lookup_opt(o + 1))) {
+	    /* No match for whole param, try each character as a short option */
 	    while (*++o) {
 		if (!(d = sopts[(unsigned char) *o])) {
 		    if (fail) {
@@ -1947,9 +1986,11 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
 		    break;
 		}
 		if (d->flags & ZOF_ARG) {
+		    /* Optarg in same parameter */
 		    if (o[1]) {
 			add_opt_val(d, o + 1);
 			break;
+		    /* Optarg in next parameter */
 		    } else if (!(d->flags & ZOF_OPT) ||
 			       (pp[1] && pp[1][0] != '-')) {
 			if (!pp[1]) {
@@ -1958,8 +1999,10 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
 			    return 1;
 			}
 			add_opt_val(d, *++pp);
+		    /* Missing optional optarg */
 		    } else
 			add_opt_val(d, NULL);
+		/* No optarg required */
 		} else
 		    add_opt_val(d, NULL);
 	    }
@@ -1972,21 +2015,36 @@ bin_zparseopts(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))
 		    break;
 	    }
 	} else {
+	    /* Whole param matches a defined option */
 	    if (d->flags & ZOF_ARG) {
 		char *e = o + strlen(d->name) + 1;
 
-		if (*e)
+		/* GNU style allows an empty optarg in the same parameter */
+		if ((d->flags & ZOF_GNU) && *e == '=') {
+		    add_opt_val(d, ++e);
+		/*
+		 * Non-empty optarg in same parameter. lookup_opt() test ensures
+		 * that this won't be a GNU-style option, where this would be
+		 * invalid
+		 */
+		} else if (*e) {
 		    add_opt_val(d, e);
-		else if (!(d->flags & ZOF_OPT) ||
-			 (pp[1] && pp[1][0] != '-')) {
+		/*
+		 * Mandatory optarg or (if not GNU style) optional optarg in
+		 * next parameter
+		 */
+		} else if (!(d->flags & ZOF_OPT) ||
+			 (!(d->flags & ZOF_GNU) && pp[1] && pp[1][0] != '-')) {
 		    if (!pp[1]) {
 			zwarnnam(nam, "missing argument for option: -%s",
 				d->name);
 			return 1;
 		    }
 		    add_opt_val(d, *++pp);
+		/* Missing optional optarg */
 		} else
 		    add_opt_val(d, NULL);
+	    /* No optarg required */
 	    } else
 		add_opt_val(d, NULL);
 	}
diff --git a/Test/V12zparseopts.ztst b/Test/V12zparseopts.ztst
index fa82a9d0b..b429a668e 100644
--- a/Test/V12zparseopts.ztst
+++ b/Test/V12zparseopts.ztst
@@ -159,16 +159,35 @@
 >ret: 0, optv: -+ -: -= -\, argv: 1 2 3
 >ret: 0, optv: --::: foo, argv: 1 2 3
 
-  for specs in '-foo: -foobar' '-foobar -foo:'; do
+  for specs in \
+    '-foo: -foobar' '-foobar -foo:' '-foo: -foobar:' '-foobar: -foo:'
+  do
     () {
       local -a optv
-      zparseopts -a optv - $=specs
+      zparseopts -a optv -D - $=specs
       print -r - ret: $?, optv: $optv, argv: $argv
     } --foobar 1 2 3
   done
-0:overlapping option specs (scan order)
->ret: 0, optv: --foobar, argv: --foobar 1 2 3
->ret: 0, optv: --foo bar, argv: --foobar 1 2 3
+0:overlapping option specs without -G (scan order)
+>ret: 0, optv: --foobar, argv: 1 2 3
+>ret: 0, optv: --foo bar, argv: 1 2 3
+>ret: 0, optv: --foobar 1, argv: 2 3
+>ret: 0, optv: --foo bar, argv: 1 2 3
+
+  for specs in \
+    '-foo: -foobar' '-foobar -foo:' '-foo: -foobar:' '-foobar: -foo:'
+  do
+    () {
+      local -a optv
+      zparseopts -a optv -D -G - $=specs
+      print -r - ret: $?, optv: $optv, argv: $argv
+    } --foobar 1 2 3
+  done
+0:overlapping option specs with -G (scan order)
+>ret: 0, optv: --foobar, argv: 1 2 3
+>ret: 0, optv: --foobar, argv: 1 2 3
+>ret: 0, optv: --foobar 1, argv: 2 3
+>ret: 0, optv: --foobar 1, argv: 2 3
 
   () {
     local -a optv
@@ -178,3 +197,66 @@
 0:missing optarg
 ?(anon):zparseopts:2: missing argument for option: -c
 >ret: 1, optv: , argv: -ab1 -c
+
+  for spec in -foo: -foo:: -foo:-; do
+    () {
+      local -a optv
+      zparseopts -a optv -D -F - $=spec
+      print -r - ret: $?, optv: $optv, argv: $argv
+    } --foo=bar 1 2 3
+  done
+0:zparseopts without -G, =optarg handling
+>ret: 0, optv: --foo =bar, argv: 1 2 3
+>ret: 0, optv: --foo=bar, argv: 1 2 3
+>ret: 0, optv: --foo=bar, argv: 1 2 3
+
+  for spec in -foo: -foo:: -foo:-; do
+    () {
+      local -a optv
+      zparseopts -a optv -D -F -G - $=spec
+      print -r - ret: $?, optv: $optv, argv: $argv
+    } --foo=bar 1 2 3
+  done
+0:zparseopts -G, single parameter, with =
+>ret: 0, optv: --foo bar, argv: 1 2 3
+>ret: 0, optv: --foo=bar, argv: 1 2 3
+>ret: 0, optv: --foo=bar, argv: 1 2 3
+
+  for spec in -foo: -foo:: -foo:-; do
+    () {
+      local -a optv
+      zparseopts -a optv -D -F -G - $=spec
+      print -r - ret: $?, optv: $optv, argv: $argv
+    } --foobar 1 2 3
+  done
+0:zparseopts -G, single parameter, without =
+?(anon):zparseopts:2: bad option: --foobar
+>ret: 1, optv: , argv: --foobar 1 2 3
+?(anon):zparseopts:2: bad option: --foobar
+>ret: 1, optv: , argv: --foobar 1 2 3
+?(anon):zparseopts:2: bad option: --foobar
+>ret: 1, optv: , argv: --foobar 1 2 3
+
+  for spec in -foo: -foo:: -foo:-; do
+    () {
+      local -a optv
+      zparseopts -a optv -D -F -G - $=spec
+      print -r - ret: $?, optv: $optv, argv: $argv
+    } --foo bar 1 2 3
+  done
+0:zparseopts -G, separate parameters
+>ret: 0, optv: --foo bar, argv: 1 2 3
+>ret: 0, optv: --foo, argv: bar 1 2 3
+>ret: 0, optv: --foo=bar, argv: 1 2 3
+
+  for spec in -foo: -foo:: -foo:-; do
+    () {
+      local -a optv
+      zparseopts -a optv -D -F -G - $=spec
+      print -r - ret: $?, optv: ${(j< >)${(q+)optv}}, argv: $argv
+    } --foo= 1 2 3
+  done
+0:zparseopts -G, empty optarg
+>ret: 0, optv: --foo '', argv: 1 2 3
+>ret: 0, optv: '--foo=', argv: 1 2 3
+>ret: 0, optv: '--foo=', argv: 1 2 3




Messages sorted by: Reverse Date, Date, Thread, Author